package org.ucas.faker.web.service.Visualization;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.ucas.faker.web.Config.VisualConfig;
import org.ucas.faker.web.fetcher.FetchExecutor;
import org.ucas.faker.web.fetcher.FetchReqBuilder;
import org.ucas.faker.web.fetcher.FetchRequest;
import org.ucas.faker.web.fetcher.FetchResult;
import org.ucas.faker.web.service.Visualization.entity.VisualParam;
import org.ucas.faker.web.service.Visualization.entity.component.Target;
import org.ucas.faker.web.service.Visualization.json.CloneUtil;
import org.ucas.faker.web.service.Visualization.json.DBUtil;
import org.ucas.faker.web.service.Visualization.json.DataUtil;
import org.ucas.faker.web.service.Visualization.json.JsonUtil;

import java.util.*;

@Service
@Transactional
public class DataService {

    @Autowired
    private FetchExecutor fetcher;

    public void parseTarget(JSONObject inputJson, VisualParam param){
        JSONArray targetArray = JsonUtil.getValue(inputJson, "target");
        String[] tmpNameArray = new String[targetArray.size()];
        String[] tmpUnitArray = new String[targetArray.size()];
        Target[] targets = new Target[targetArray.size()];
        for (int k=0;k<tmpNameArray.length;k++){
            tmpNameArray[k] = targetArray.getJSONObject(k).getString("name");
            tmpUnitArray[k] = targetArray.getJSONObject(k).getString("unit");
            Target thisTarget = new Target();
            thisTarget.setName(tmpNameArray[k]);
            thisTarget.setUnit(tmpUnitArray[k]);
            targets[k] = thisTarget;
        }
        param.setTarget(targets[0]);
        param.setMultiTarget(targets);
        param.setTargetDimension(tmpNameArray.length);
    }

    public void parseBasisDimension(JSONObject inputJson, VisualParam param){
        int dimension = getBasisDimension(inputJson);
        param.setBasisDimension(dimension);
    }

    public int getBasisDimension(JSONObject inputJson){
        int dimension = 2;
        if (inputJson.containsKey("branch")) {
            dimension = 3;
        }
        return dimension;
    }

    public void parseData2D1T(JSONObject inputJson, FetchReqBuilder requestBuilder, VisualParam param){
        String basisKey = JsonUtil.getValue(inputJson, "basis.key");
        parseBasisLabelAndRangeAndOrder(inputJson, requestBuilder, param);

        if (inputJson.containsKey("branch")) {
            Object branchLabel = JsonUtil.getValue(inputJson, "branch.label");
            if (branchLabel instanceof JSONArray) {
                param.setBranchLabel((JSONArray) branchLabel);
            }
            String branchName = JsonUtil.getValue(inputJson, "branch.name");
            param.setBranchName(branchName);
            String branchUnit = JsonUtil.getValue(inputJson, "branch.unit");
            if (branchUnit != null) {
                param.setBranchUnit(branchUnit);
            }
        }

        String branchKey = inputJson.getJSONObject("branch").getString("key");

        requestBuilder.select(basisKey, branchKey).count("*", "num").group(basisKey, branchKey)
                .not_null(basisKey).not_null(branchKey);
        System.out.println("2D1T SQL: "+requestBuilder.build().sql());
        FetchRequest req = requestBuilder.build();
        FetchResult result = fetcher.fetch(req);

//        System.out.println(result.size());

        List basisValueList = result.column(basisKey);
        List branchValueList = result.column(branchKey);
        List countList = result.column("num");

        for (int i=0;i<basisValueList.size();i++){
            System.out.println(basisValueList.get(i)+"\t"+branchValueList.get(i)+"\t"+countList.get(i));
        }

        String branchRangeType = JsonUtil.getValue(inputJson, "branch.range.type");
        JSONArray branchRange = null;
        switch (branchRangeType){
            case VisualConfig.Json.Value.RANGE_TYPE_AUTO:
                int topNum = JsonUtil.getValue(inputJson, "branch.range.value");
                Map<String, Long> countBranchMap = topCountElement(branchValueList, countList, topNum);
                JSONArray autoBranchLabel = getAutoBranchLabel(countBranchMap);
                param.setBranchLabel(autoBranchLabel);
                branchRange = getAutoBranchRange(countBranchMap, branchValueList);
                break;
            case VisualConfig.Json.Value.RANGE_TYPE_HYPERSET:
                branchRange = JsonUtil.getValue(inputJson, "branch.range.value");
                break;
            case VisualConfig.Json.Value.RANGE_TYPE_SET:
                branchRange = JsonUtil.getValue(inputJson, "branch.range.value");
                param.setBranchLabel((JSONArray) branchRange);
                break;
            default:
                break;
        }

        List<Integer>[] dataArray = new List[branchRange.size()];
        for (int i=0; i<branchRange.size();i++) {
            dataArray[i] = getDataList(basisValueList, branchValueList, countList, branchRange.get(i));
        }

        param.setDataArray(dataArray);
        param.setBasisLabel(JsonUtil.listToJsonArray(getUniqueElementList(basisValueList)));
//        combineBasisHyperset(inputJson, param);
    }

    public void parseData1D2T(JSONObject inputJson, FetchReqBuilder requestBuilder, VisualParam param){
        String basisKey = JsonUtil.getValue(inputJson, "basis.key");
        parseBasisLabelAndRangeAndOrder(inputJson, requestBuilder, param);

        JSONArray targetArray = JsonUtil.getValue(inputJson, "target");
        int targetNum = targetArray.size();
        String[] targetName = new String[targetNum];
        String[] targetKey = new String[targetNum];
        FetchReqBuilder[] rbs = new FetchReqBuilder[targetNum];
        for (int i=0;i<targetNum;i++){
            targetName[i] = targetArray.getJSONObject(i).getString("name");
            targetKey[i] = targetArray.getJSONObject(i).getString("key");
            rbs[i] = CloneUtil.clone(requestBuilder);
        }

        List[] basisListArray = new List[targetNum];
        List[] targetListArray = new List[targetNum];

        for (int i=0;i<targetNum;i++){
            String targetDBKey = VisualConfig.getTargetDBKey(targetKey[i]);
            rbs[i].select(basisKey, targetDBKey).group(basisKey, targetDBKey);
            System.out.println("1D2T SQL:"+rbs[i].build().sql());
            FetchResult result = fetcher.fetch(rbs[i].build());
            basisListArray[i] = result.column(basisKey);
            targetListArray[i] = result.column(DBUtil.getColumnName(targetDBKey));


 /*           for (int j=0;j<basisListArray[i].size();j++){
                System.out.println(basisListArray[i].get(j)+"\t"+targetListArray[i].get(j));
            }*/

        }

        Map<Object, Object[]> dataMap = new TreeMap<Object, Object[]>();
        for (int i=0;i<targetNum;i++){
            for (int j=0;j<basisListArray[i].size();j++){
                Object basisLabel = basisListArray[i].get(j);
                if (!dataMap.containsKey(basisLabel)){
                    Object[] obj = new Object[targetNum];
                    for (int k=0;k<targetNum;k++){
                        obj[k] = 0L;
                    }
                    dataMap.put(basisLabel, obj);
                }
                Object[] value = dataMap.get(basisLabel);
                value[i] = addValues(value[i], 1L);
                dataMap.put(basisLabel, value);
            }
        }

        dataMap = DataUtil.sortMapByKey(dataMap, true);

/*        for (Object key : dataMap.keySet()){
            for (Object value : dataMap.get(key)){
                System.out.print(value+", ");
            }
            System.out.println();
        }*/

        dataMap = combineBasisHyperset(inputJson, dataMap);

        List multiDimentionData = new ArrayList();
        for (Object key : dataMap.keySet()){
            Object[] tmp = new Object[dataMap.get(key).length+1];
            for (int k=0;k<dataMap.get(key).length;k++){
                tmp[k] = dataMap.get(key)[k];
            }
            tmp[dataMap.get(key).length] = key;
            multiDimentionData.add(tmp);
        }
        param.setMultiDimensionData(multiDimentionData);
    }


    public void parseData1D1T(JSONObject inputJson, FetchReqBuilder requestBuilder, VisualParam param){
        String basisKey = JsonUtil.getValue(inputJson, "basis.key");
        parseBasisLabelAndRangeAndOrder(inputJson, requestBuilder, param);

        requestBuilder.select(basisKey).count("*", DBUtil.keyCount(basisKey)).group(basisKey).not_null(basisKey);
        System.out.println("1D1T SQL: "+requestBuilder.build().sql());
        FetchResult result = fetcher.fetch(requestBuilder.build());

        List countList = result.column(DBUtil.keyCount(basisKey));
        List keyList = result.column(basisKey);

        for (int i=0;i<keyList.size();i++){
            System.out.println(keyList.get(i)+"\t"+countList.get(i));
        }

        param.setBasisLabel(JsonUtil.listToJsonArray(keyList));
        param.setDataList(countList);
        combineBasisHyperset(inputJson, param);
    }

    /**
     * combine for 1D2T
     */
    protected Map<Object, Object[]> combineBasisHyperset(JSONObject inputJson, Map<Object, Object[]> dataMap){
        if (!basisHypersetCombinable(inputJson)){
            return dataMap;
        }
        JSONArray basisLabel = JsonUtil.getValue(inputJson, "basis.label");
        JSONArray basisRangeSet = JsonUtil.getValue(inputJson, "basis.range.value");

        TreeMap<Object, Object[]> resultMap = new TreeMap<Object, Object[]>();
        for (int j=0;j<basisLabel.size();j++){
            List valueSet = getValueSetInRange(basisRangeSet.get(j));
            Object[] templateElement = dataMap.get(dataMap.keySet().iterator().next());
            Object[] newValue = new Object[templateElement.length];
            for (int k=0;k<newValue.length;k++) {
                newValue[k] = castNumber(0, templateElement[k].getClass());
            }
            for (Object dataKey : dataMap.keySet()){
                if (valueSet.contains(dataKey)){
                    Object[] dataValue = dataMap.get(dataKey);
                    for (int t=0;t<dataValue.length;t++) {
                        newValue[t] = addValues(newValue[t], dataValue[t]);
                    }
                }
            }
            resultMap.put(basisLabel.get(j), newValue);
        }
        return resultMap;
    }

    /**
     *  combine for 1D1T and 2D1T
     */
    protected void combineBasisHyperset(JSONObject inputJson, VisualParam param){
        if (!basisHypersetCombinable(inputJson)){
            return;
        }

        JSONArray originalBasisLabel = param.getBasisLabel();

        System.out.println(JsonUtil.getValue(inputJson, "basis.range.value").toString());
        JSONArray basisRangeSet = JsonUtil.getValue(inputJson, "basis.range.value");

        JSONArray basisLabel = null;
        if (JsonUtil.containsKey(inputJson, "basis.label")){

            if (JsonUtil.getValue(inputJson, "basis.label") instanceof JSONArray){
                basisLabel = JsonUtil.getValue(inputJson, "basis.label");
            } else {
                basisLabel = basisRangeSet;
            }
        } else {
            basisLabel = basisRangeSet;
        }

        List[] dataArray = param.getDataArray();
        List[] newDataArray = new ArrayList[dataArray.length];

        for (int i=0;i<dataArray.length;i++){
            List dataList = dataArray[i];
            List newDataList = new ArrayList();
            for (int j=0;j<basisLabel.size();j++){
                Object range = basisRangeSet.get(j);

                List valueSet = getValueSetInRange(range);

                Object newValue = 0L;
                if (dataList.size()>0) {
                    castNumber(0, dataList.get(0).getClass());
                }

                for (int k=0;k<dataList.size();k++){
                    Object value = dataList.get(k);
                    if (valueSet.contains(originalBasisLabel.get(k))){
                        newValue = addValues(newValue, value);
                    }
                }
                newDataList.add(newValue);
            }
            newDataArray[i] = newDataList;
        }
        param.setDataArray(newDataArray);
        param.setBasisLabel(basisLabel);
    }

    private boolean basisHypersetCombinable(JSONObject inputJson){
        String basisRangeType = JsonUtil.getValue(inputJson, "basis.range.type");
        if (basisRangeType == null){
            return false;
        }
        Object tmpbasisLabel = JsonUtil.getValue(inputJson, "basis.label");
        if (basisRangeType.equals(VisualConfig.Json.Value.RANGE_TYPE_HYPERSET)) {
            if (tmpbasisLabel == null) {
                return false;
            }
            if (!(tmpbasisLabel instanceof JSONArray)) {
                return false;
            }
        }

        if (!basisRangeType.equals(VisualConfig.Json.Value.RANGE_TYPE_HYPERSET) &&
                !basisRangeType.equals(VisualConfig.Json.Value.RANGE_TYPE_SET)){
            return false;
        }
        return true;
    }

    /**
     * @param range belongs to JSONObject or JSONArray
     */
    private List getValueSetInRange(Object range){
        List valueSet = new ArrayList();
        if (range instanceof JSONArray) {
            for (int k=0;k<((JSONArray)range).size();k++){
                Object el = ((JSONArray) range).get(k);
                if (el instanceof JSONArray){
                    for (int t=0;t<((JSONArray) el).size();t++){
                        valueSet.add(((JSONArray) el).get(t));
                    }
                } else {
                    valueSet.add(((JSONArray) range).get(k));
                }
            }
        } else {
            valueSet.add(range);
        }
        return valueSet;
    }

    private Object castNumber(Object number, Class<?> clazz){
        switch (clazz.getName()){
            case "java.lang.Long":
                return Long.parseLong(""+number);
            case "java.lang.Double":
                return Double.parseDouble(""+number);
            case "java.lang.Integer":
                return Integer.parseInt(""+number);
            default:
                return Long.parseLong(""+number);
        }
    }

    private Object addValues(Object x, Object y){
        Class<?> classX = x.getClass();
        int numX = -1;
        Class<?> classY = y.getClass();
        int numY = -1;
        Class<?>[] classSet = {Double.class, Long.class, Integer.class};
        for (int i=0;i<classSet.length;i++){
            if (classX.getName().equals(classSet[i].getName())){
                numX = i;
            }
            if (classY.getName().equals(classSet[i].getName())){
                numY = i;
            }
        }
        if (numX == -1 || numY == -1){
            return Integer.parseInt("0");
        }
        int num = Math.min(numX, numY);
        String xs = x.toString();
        String ys = y.toString();

        switch (num){
            case 0:
                return Double.parseDouble(xs) + Double.parseDouble(ys);
            case 1:
                return Long.parseLong(xs) + Long.parseLong(ys);
            case 2:
                return Integer.parseInt(xs) + Integer.parseInt(ys);
            default:
                return 0;
        }
    }

    public void parseBasisLabelAndRangeAndOrder(JSONObject inputJson, FetchReqBuilder requestBuilder, VisualParam param){
        String basisKey = JsonUtil.getValue(inputJson, "basis.key");
        boolean basisKeyOrderType = false;
        String basisKeyOrderValue = VisualConfig.Json.Value.BASIS_ORDER_ASC;

        if (JsonUtil.containsKey(inputJson, "basis.range")){
            String basisRangeType = JsonUtil.getValue(inputJson, "basis.range.type");
            Object basisRangeValue = JsonUtil.getValue(inputJson, "basis.range.value");

            switch (basisRangeType){
                case VisualConfig.Json.Value.RANGE_TYPE_AUTO:
                    int topNum = Integer.parseInt(basisRangeValue.toString());
                    if (param.getBasisDimension() == 2) {
                        requestBuilder.top(DBUtil.keyCount(basisKey), topNum);
                        basisKeyOrderType = true;
                        basisKeyOrderValue = VisualConfig.Json.Value.BASIS_ORDER_T_DESC;
                    } else if (param.getBasisDimension() == 3){
                        FetchReqBuilder tmpRequestBuilder = FetchReqBuilder.instance().depot(param.getDepotId());
                        tmpRequestBuilder.select(basisKey).count("*", DBUtil.keyCount(basisKey)).group(basisKey).not_null(basisKey).top(DBUtil.keyCount(basisKey), topNum);
                        FetchResult tmpResult = fetcher.fetch(tmpRequestBuilder.build());
                        if ( tmpResult.column(basisKey).size()>0) {
                            requestBuilder.in(basisKey, tmpResult.column(basisKey));
                        }
                    }
                    break;
                case VisualConfig.Json.Value.RANGE_TYPE_INTERVAL:
                    JSONArray interval = (JSONArray)basisRangeValue;
                    requestBuilder.ge(basisKey, interval.get(0)).le(basisKey, interval.get(1));
                    break;
                case VisualConfig.Json.Value.RANGE_TYPE_HYPERSET:
                case VisualConfig.Json.Value.RANGE_TYPE_SET:
                    List valueSet = getValueSetInRange(basisRangeValue);
                    System.out.println("size====="+valueSet.size());
                    if (valueSet.size()>0) {
                        requestBuilder.in(basisKey, valueSet);
                    }
                    break;
                case VisualConfig.Json.Value.RANGE_TYPE_TOP:
                    break;
                default:
                    break;
            }
        }

        if (JsonUtil.containsKey(inputJson, "basis.order")){
            basisKeyOrderType = true;
            basisKeyOrderValue = JsonUtil.getValue(inputJson, "basis.order");
        }

        System.out.println("order="+basisKeyOrderValue);

        if (basisKeyOrderType == true){
            switch (basisKeyOrderValue){
                case VisualConfig.Json.Value.BASIS_ORDER_ASC:
                    requestBuilder.order(true, basisKey);
                    break;
                case VisualConfig.Json.Value.BASIS_ORDER_DESC:
                    requestBuilder.order(false, basisKey);
                    break;
                case VisualConfig.Json.Value.BASIS_ORDER_T_ASC:
                    if (param.getBasisDimension() == 2) {
                        requestBuilder.order(true, DBUtil.keyCount(basisKey));
                    }
                    break;
                case VisualConfig.Json.Value.BASIS_ORDER_T_DESC:
                    if (param.getBasisDimension() == 2) {
                        requestBuilder.order(false, DBUtil.keyCount(basisKey));
                    }
                    break;
                default:
                    break;
            }
        }
    }

    public void parseConstraint(JSONObject inputJson, FetchReqBuilder requestBuilder){
        if (JsonUtil.containsKey(inputJson, "constraints")){
            JSONArray constraintArray = JsonUtil.getValue(inputJson, "constraints");
            for (int i=0;i<constraintArray.size();i++){
                JSONObject constraintJson = constraintArray.getJSONObject(i);
                String constraintType = constraintJson.getString("type");
                String constraintKey = constraintJson.getString("key");
                Object constraintValue = constraintJson.get("value");

                switch (constraintType){
                    case VisualConfig.Json.Value.CONSTRAINT_TYPE_EQUAL:
                        requestBuilder.eq(constraintKey, constraintValue);
                        break;
                    case VisualConfig.Json.Value.CONSTRAINT_TYPE_TOPSUM:
                        //TODO
                    default:
                        break;
                }
            }
        }
    }

    private List getDataList(List basisValueList, List branchValueList, List countList, Object range){
        List branchDataList = new ArrayList();
        Map<String, Long> countMap = new HashMap<String, Long>();

        List valueSet = new ArrayList();
        if (range instanceof JSONArray) {
            for (int k=0;k<((JSONArray)range).size();k++){
                valueSet.add(((JSONArray) range).get(k));
            }
        } else {
            valueSet.add(range);
        }

        for (int j = 0; j < basisValueList.size(); j++) {
            String tmpKey = basisValueList.get(j).toString();
            if (!countMap.containsKey(tmpKey)){
                countMap.put(tmpKey, 0L);
            }

            if (valueSet.contains(branchValueList.get(j))) {
                long cnt = countMap.get(tmpKey);
                long ccnt = Long.parseLong(countList.get(j).toString());
                countMap.put(tmpKey, cnt + ccnt);
            }
        }

        List<String> basisValueIdenticalList = new ArrayList<String>();


        for (int j=0;j<basisValueList.size();j++){
            String tmpKey = basisValueList.get(j).toString();
            if (basisValueIdenticalList.contains(tmpKey)){
                continue;
            } else {
                basisValueIdenticalList.add(tmpKey);
            }
            if (countMap.containsKey(tmpKey)){
                branchDataList.add(countMap.get(tmpKey));
            } else {
                branchDataList.add(0);
            }
        }
        return branchDataList;
    }

    private Map<String, Long> countListElement(List valuelist, List countList){
        Map<String, Long> countMap = new HashMap<String, Long>();
        for (int i=0;i<valuelist.size();i++){
            String element = valuelist.get(i).toString();
            if (!countMap.containsKey(element)){
                countMap.put(element, 0L);
            }

            long cnt = countMap.get(element);
            long ccnt = Long.parseLong(countList.get(i).toString());
            countMap.put(element, cnt+ccnt);
        }
        return countMap;
    }

    private Map<String, Long> sortMap(Map<String, Long> map){
        List<Map.Entry<String,Long>> list = new ArrayList<Map.Entry<String,Long>>(map.entrySet());
        Collections.sort(list,new Comparator<Map.Entry<String,Long>>() {
            public int compare(Map.Entry<String, Long> o1, Map.Entry<String, Long> o2) {
                return o2.getValue().compareTo(o1.getValue());
            }

        });
        Map<String, Long> resultMap = new TreeMap<String, Long>();
        for (Map.Entry<String,Long> entry : list){
            resultMap.put(entry.getKey(), entry.getValue());
        }
        return resultMap;
    }

    private Map<String, Long> topMap(Map<String, Long> map, int top){
        List<Map.Entry<String,Long>> list = new ArrayList<Map.Entry<String,Long>>(map.entrySet());
        Collections.sort(list,new Comparator<Map.Entry<String,Long>>() {
            public int compare(Map.Entry<String, Long> o1, Map.Entry<String, Long> o2) {
                return (int)(o2.getValue() - o1.getValue());
            }

        });
        Map<String, Long> resultMap = new HashMap<String, Long>();
        for (int i=0;i<Math.min(top, list.size());i++){
            resultMap.put(list.get(i).getKey(), list.get(i).getValue());
        }
        return resultMap;
    }

    private Map<String, Long> topCountElement(List valuelist, List countList, int top){
        Map<String, Long> map = countListElement(valuelist, countList);
        map = topMap(map, top);
        return map;
    }

    private JSONArray getAutoBranchLabel(Map<String, Long> map ){
        JSONArray branchLabel = new JSONArray();
        for (String key : map.keySet()){
            branchLabel.add(key);
        }
        return branchLabel;
    }

    private <T> JSONArray getAutoBranchRange(Map<String, Long> map, List<T> branchValueList){
        JSONArray branchRange = new JSONArray();
        for (String key : map.keySet()){
            for (Object obj : branchValueList){
                if (obj.toString().equals(key)){
                    branchRange.add(obj);
                    break;
                }
            }
        }
        return branchRange;
    }

    private <T> List<T> getUniqueElementList(List<T> list){
        List<T> resultList = new ArrayList<T>();
        for (T t : list){
            if (!resultList.contains(t)){
                resultList.add(t);
            }
        }
        return resultList;
    }
}
